package com.xzz.mybatis.generator.quartz;

import com.xzz.mybatis.generator.utils.MyAopTargetUtils;
import com.xzz.mybatis.generator.utils.MyApplicationContextUtil;
import com.xzz.mybatis.generator.utils.MyCollectionUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdScheduler;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: hhz
 * @createDate: 2021-3-29
 */
@Slf4j
@Component
public class MyQuartzBeanPostProcessor implements BeanPostProcessor, InitializingBean, Ordered,
        ApplicationListener<ContextRefreshedEvent>{
    private Map<JobDetail, Trigger> taskMap = new LinkedHashMap<>();
    private static Map<String, String> runLocalMap = new ConcurrentHashMap<>();

    private static final String QUARTZ_DEFAULT_GROUP_NAME = "DEFAULT";
    private static final String QUARTZ_DEFAULT_RUN_ALL = "all";

    public Map<JobDetail, Trigger> getTaskMap(){
        return taskMap;
    }
    public Set<JobKey> getAllLocalJobKeys(){
        Set<JobKey> jobKeys = new LinkedHashSet<>();
        taskMap.keySet().forEach(jobDetail -> jobKeys.add(jobDetail.getKey()));
        return jobKeys;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // do nothing yet
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }
    @SneakyThrows
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 容器bean依赖都准备好后, 开始通过 schedluer 注册quartz trigger 和 job detail
        if(event.getApplicationContext() == MyApplicationContextUtil.getApplicationContext()) {
            Scheduler scheduler = MyApplicationContextUtil.getBean(Scheduler.class);
            if(scheduler!=null && !taskMap.isEmpty() ){
                //获取所有default组的job，之后去匹配当前bizService注解，干掉去掉注解的job
                tryCleanOldJobs(scheduler);

                tryFilterNewJobs();

                for(Map.Entry<JobDetail, Trigger> entry : taskMap.entrySet()){
                    initScheduleJobs(scheduler, entry);
                }
            }
        }
    }
    private void tryCleanOldJobs(Scheduler scheduler) throws SchedulerException {
        /** 开启本地跑，先不清理, 可以保证不清理别人正在开发的定时任务,
         * 但是集群情况下不能保证同一数据库，其他服务器实例重启，而跑了清理动作, 这个时候本地开发机器只能重启本地应用，重建定时任务
         * 具体见 qrtz_triggers， qrtz_job_details， qrtz_cron_triggers 这几张表，查看自己的job和trigger情况
         * 所以开发测试环境暂用内存单例quartz模式, uat和生产到时候用集群, 这样就避免了问题..,
         * !!!但是注意到生产环境调试，一定要重新用一份新的nacos配置，启用单例quartz模式!!!
         * **/
        if(!(scheduler instanceof StdScheduler) || (checkRunQuartzLocal() && !checkRunQuartzAll())) return;
        StdScheduler stdScheduler = (StdScheduler)scheduler;
        Set<JobKey> allJobKeys = stdScheduler.getJobKeys(GroupMatcher.groupEquals(QUARTZ_DEFAULT_GROUP_NAME));
        if(MyCollectionUtil.isEmptyOrNull(allJobKeys)) return;
        allJobKeys.forEach(jobKey->{
            try {
                JobDetail jobDetail = stdScheduler.getJobDetail(jobKey);
                String beanName = jobDetail.getJobDataMap().getString(InternJob.BEAN_NAME);
                String methodName = jobDetail.getJobDataMap().getString(InternJob.METHOD_NAME);
                Object bizSvcInst = MyApplicationContextUtil.getBeanByName(beanName);
                if(null!=bizSvcInst) { bizSvcInst = MyAopTargetUtils.getTarget(bizSvcInst); } //尝试剥离aop代理获取具体实例
                if(null==bizSvcInst){ // bizservice没在容器中
                    cleanOneJob(scheduler, jobKey);
                    return;
                }
                cleanJobWhenMethodNotExistOrNotAnnotated(scheduler, jobKey, methodName, bizSvcInst);
            } catch (Exception e) {
                log.warn("clean {} job failed", jobKey.getName());
            }
        });
    }

    private void tryFilterNewJobs() {
        if(checkRunQuartzLocal() && !checkRunQuartzAll()){
            for(Iterator<Map.Entry<JobDetail, Trigger>> it = taskMap.entrySet().iterator(); it.hasNext();){
                Map.Entry<JobDetail, Trigger> entry = it.next();
                String beanName = entry.getKey().getJobDataMap().getString(InternJob.BEAN_NAME);
                String methodName = entry.getKey().getJobDataMap().getString(InternJob.METHOD_NAME);
                if(runLocalMap.get(methodName)==null||
                        !beanName.equalsIgnoreCase(runLocalMap.get(methodName))){
                    //beanName和methodName 存在任何一个不相等，则删除此任务，不再schdule
                    it.remove();
                }
            }
        }
    }
    private void initScheduleJobs(Scheduler scheduler, Map.Entry<JobDetail, Trigger> entry) throws SchedulerException {
        //jobdetail 和 trigger绑定
        if(!scheduler.checkExists(entry.getKey().getKey())) {
            scheduler.scheduleJob(entry.getKey(), entry.getValue());
        }
        JdbcTemplate jdbcTemplate = MyApplicationContextUtil.getBean(JdbcTemplate.class);
        List<Map<String,Object>> customRecords = jdbcTemplate.queryForList("select job_name,cron_expression from qrtz_cron_custom");
        Map<String,String> jobAndCronMap = new HashMap<>(customRecords.size());
        if(MyCollectionUtil.isNotEmptyOrNull(customRecords)){
            for(Map<String,Object> record : customRecords){
                jobAndCronMap.put((String)record.get("job_name"), (String)record.get("cron_expression"));
            }
        }
        //强制更新trigger, 解决cron表达式不更新, 如果已经被停止的任务则忽略
        if(scheduler.checkExists(entry.getValue().getKey())
                && !Trigger.TriggerState.PAUSED.equals(scheduler.getTriggerState(entry.getValue().getKey()))) {
            String jobName = entry.getKey().getKey().getName();
            if(jobAndCronMap.containsKey(jobName)){ //已经自定义定时频率
                Trigger newTrigger = TriggerBuilder.newTrigger().
                        withSchedule(CronScheduleBuilder.cronSchedule(jobAndCronMap.get(jobName)))
                        .withIdentity(jobName+"-trigger")
                        .build();
                scheduler.rescheduleJob(entry.getValue().getKey(), newTrigger);
            }else {
                scheduler.rescheduleJob(entry.getValue().getKey(), entry.getValue());
            }
        }
    }
    /** true 开启本地跑 */
    private static boolean checkRunQuartzLocal(){
        QuartzProperties quartzProperties = MyApplicationContextUtil.getBean(QuartzProperties.class);
        if(quartzProperties.getEpQuartzLocal()==null||quartzProperties.getEpQuartzLocal().length==0){
            log.warn("----------->本地暂未开启跑quartz, " +
                    "如需开启请添加应用启动参数 --spring.quartz.epQuartzLocal" +
                    "=bizServiceName1#methodName1,bizServiceName2#methodName2 多个service和方法用逗号隔开");
            return false;
        }
        if(runLocalMap.isEmpty()){
            for(String job : quartzProperties.getEpQuartzLocal()){
                String[] jobSvcAndMethod = job.trim().split("#");
                runLocalMap.put(jobSvcAndMethod[1], jobSvcAndMethod[0]);
            }
        }
        return true;
    }
    /** --spring.quartz.epQuartzLocal=all#all 跑所有，需要配置在服务器启动参数上 */
    private static boolean checkRunQuartzAll(){
        return runLocalMap.get(QUARTZ_DEFAULT_RUN_ALL)!=null;
    }

    private void cleanOneJob(Scheduler scheduler, JobKey jobKey){
        try {
            scheduler.pauseJob(jobKey); //尝试暂停job
        }catch (SchedulerException e){
            if(log.isDebugEnabled()){
                log.debug("pause {} failed", jobKey.getName());
            }
        }
        try {
            scheduler.deleteJob(jobKey);
        }catch (SchedulerException e){
            log.warn("clean {} job failed", jobKey.getName());
        }
    }

    private void cleanJobWhenMethodNotExistOrNotAnnotated
            (Scheduler scheduler, JobKey jobKey, String methodName, Object bizSvcInst) {
        try {
            Method method = bizSvcInst.getClass().getDeclaredMethod(methodName, null);
            if (!method.isAnnotationPresent(MyScheduled.class)) { //EpScheduled已经取消注解
                cleanOneJob(scheduler, jobKey);
            }
        }catch (NoSuchMethodException|SecurityException e){
            //没找到方法，方法名称变更
            cleanOneJob(scheduler, jobKey);
        }
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof AopInfrastructureBean) { //忽略aop框架相关产生的bean
            return bean;
        }

        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
        Map<Method, MyScheduled> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                (MethodIntrospector.MetadataLookup<MyScheduled>) method -> AnnotatedElementUtils.getMergedAnnotation(
                        method, MyScheduled.class)
        );
        if(!annotatedMethods.isEmpty()){
            for(Map.Entry<Method, MyScheduled> entry : annotatedMethods.entrySet()) {
                Assert.isTrue(entry.getKey().getParameterCount() == 0,
                        "Only no-arg methods can be annotated with @MyScheduled");

                Trigger trigger = TriggerBuilder.newTrigger().
                        withSchedule(CronScheduleBuilder.cronSchedule(entry.getValue().cron()))
                        .withIdentity(entry.getValue().jobEnum().getName()+"-trigger")
                        .build();

                JobDetail jobDetail = JobBuilder.newJob(InternJob.class)
                        .usingJobData(InternJob.BEAN_NAME, beanName)
                        .usingJobData(InternJob.METHOD_NAME, entry.getKey().getName())
                        .withIdentity(entry.getValue().jobEnum().getName())
                        .withDescription(entry.getValue().jobEnum().getDesc())
                        .storeDurably()
                        .requestRecovery()
                        .build();
                taskMap.put(jobDetail, trigger);

                if (log.isDebugEnabled()) {
                    log.debug("@MyScheduled method '{}' processed on bean '{}'",
                            entry.getKey().getName(), beanName);
                }
            }
        }

        return bean;
    }

    public static class InternJob implements Job{
        static final String BEAN_NAME = "beanName";
        static final String METHOD_NAME = "methodName";

        @SneakyThrows
        @Override
        public void execute(JobExecutionContext context) {
            if(!checkRunQuartzLocal()){ return; }
            String beanName = context.getMergedJobDataMap().getString(BEAN_NAME);
            String methodName = context.getMergedJobDataMap().getString(METHOD_NAME);
            if(!checkRunQuartzAll() &&
                    (runLocalMap.get(methodName)==null||
                            !beanName.equalsIgnoreCase(runLocalMap.get(methodName))) ){
                //beanName和methodName 存在任何一个不相等，则不继续跑任务
                return;
            }

            QrtzRunningLogService logService = MyApplicationContextUtil.getBean(QrtzRunningLogService.class);
            QrtzRunningLogDTO newLogDto = new QrtzRunningLogDTO(
                    context.getJobDetail().getKey().getName(),null,null,null,null);
            Long startTime = System.currentTimeMillis();
            try {
                Object bean = MyApplicationContextUtil.getBeanByName(beanName);
                Method method = bean.getClass().getMethod(methodName);
                method.invoke(bean, null);
                newLogDto.setRunningState(true);
                newLogDto.setSpendTime((int)((System.currentTimeMillis()-startTime)/1000));
                logService.saveNewLog(newLogDto);
            }catch (Exception e){
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                e.printStackTrace(pw);
                newLogDto.setRunningState(false);
                newLogDto.setErrorMsg(sw.toString());
                newLogDto.setSpendTime((int)((System.currentTimeMillis()-startTime)/1000));
                logService.saveNewLog(newLogDto);
                log.info(newLogDto.getJobName()+"运行失败,请到管理界面或服务器日志查看原因");
                throw  e;
            }
        }
    }
}
